/**
 * @author Cator Vee  hi@catorv.com
 */
const OBJECT_PROTOTYPE = Object.prototype
const ARRAY_PROTOTYPE = Array.prototype
const EXP_TYPE = /\s([a-z|A-Z]*)/
const EXP_TPL_TAG = /{(.*?)}/g
const HTML_ENTITIES = [
  '&', '&amp;',
  '<', '&lt;',
  '>', '&gt;',
  ' ', '&nbsp;',
  '\'', '&#39;',
  '"', '&quot;'
]

export const nop = function () {
}
export const isString = _checkType('string')
export const isFunction = _checkType('function')
export const isObject = _checkType('object')
export const isNumber = obj => getType(obj) === 'number' && !isNaN(obj)

export function mixin (target) {
  const args = arguments
  let len = args.length
  let i = 1
  let overwrite, source

  target = target || {}

  if (len > 1) {
    if (typeof args[len - 1] === 'boolean') {
      overwrite = args[--len]
    }

    while (i < len) {
      (source = args[i++]) && Object.keys(source).forEach(name => {
        if (defined(source[name]) && (overwrite || !defined(target[name]))) {
          target[name] = source[name]
        }
      })
    }
  }

  return target
}

export function defined (obj) {
  return typeof obj !== 'undefined'
}

export function getType (obj) {
  return OBJECT_PROTOTYPE.toString.call(obj).match(EXP_TYPE)[1].toLowerCase()
}

export function rand (min, max) {
  if (!defined(max)) {
    max = min
    min = 0
  }
  return min + (Math.random() * (max - min)) | 0
}

export function forEach (obj, fn, context) {
  if (obj) {
    if (Array.isArray(obj)) {
      ARRAY_PROTOTYPE.forEach.call(obj, fn, context)
    } else {
      Object.keys(obj).forEach(key => {
        fn.call(context, obj[key], key, obj)
      })
    }
  }
}

export function map (obj, fn, context) {
  if (obj) {
    if (Array.isArray(obj)) {
      return ARRAY_PROTOTYPE.map.call(obj, fn, context)
    } else {
      let result = {}
      forEach(obj, function (value, key, object) {
        result[key] = fn.call(context, value, key, object)
      })
      return result
    }
  }
}

export function tpl (str, obj, noMatched) {
  return str.replace(EXP_TPL_TAG, function (matched, content) {
    if (content) {
      const value = getObject(content, obj)
      return defined(value) ? value : (defined(noMatched) ? noMatched : matched)
    }
    return matched
  })
}

export function htmlEncode (str) {
  return str.replace(/[&<>'"]/g, function (matched) {
    return HTML_ENTITIES[HTML_ENTITIES.indexOf(matched) + 1]
  })
}

export function htmlDecode (str) {
  return str.replace(/&(?:amp|lt|gt|nbsp|#39|quot);/g, function (matched) {
    return HTML_ENTITIES[HTML_ENTITIES.indexOf(matched) - 1]
  })
}

export function getObject (pkgname, context) {
  const keys = pkgname.split('.')
  const len = keys.length
  let i = 0
  let obj = context || global
  while (obj && i < len) {
    obj = obj[keys[i++]]
  }
  return obj
}

export function resolveNames (config) {
  if (config) {
    if (isObject(config)) {
      let result = {}
      forEach(config, (value, key) => {
        key = key.replace(/-[a-z]/g, s => s.substring(1).toUpperCase())
        result[key] = resolveNames(value)
      })
      return result
    } else if (Array.isArray(config)) {
      return config.map(resolveNames)
    }
  }
  return config
}

function _checkType (type) {
  return function (obj) {
    return getType(obj) === type
  }
}

// Extend Array Object
Array.from = (obj, mapFn, context) => {
  let array = ARRAY_PROTOTYPE.slice.call(obj)
  if (isFunction(mapFn)) {
    return array.map(mapFn, context)
  }
  return array
}
mixin(ARRAY_PROTOTYPE, {
  includes: function (searchElement, fromIndex) {
    fromIndex = fromIndex | 0
    return this.indexOf(searchElement, fromIndex) >= fromIndex
  },
  find: function (finderFn) {
    return this.filter(finderFn)[0]
  },
  remove: function (element) {
    let index = this.indexOf(element)
    if (index >= 0) {
      return this.splice(index, 1)[0]
    }
  }
})

// Extend Function Object
mixin(Function.prototype, {
  defer: function (millis) {
    const args = ARRAY_PROTOTYPE.slice.call(arguments, 1)
    return (
      this._job = setTimeout(() => {
        this.apply(null, args)
        delete this._job
      }, millis)
    )
  },

  cancel: function () {
    clearTimeout(this._job)
    delete this._job
  },

  buffer: function (millis) {
    if (!this._job) {
      this.defer.apply(this, arguments)
    }
  }
})

// Extend Date Object
Date.WEEK_NAMES = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
Date.MONTH_NAMES = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
mixin(Date.prototype, {
  format: function (format, fn) {
    const marks = {
      'y': this.getFullYear(), // 年份
      'm': this.getMonth() + 1, // 月份
      'M': Date.MONTH_NAMES[this.getMonth()], // 月份 名称
      'd': this.getDate(), // 日
      'h': this.getHours() % 12, // 小时 12小时制
      'H': this.getHours(), // 小时 24小时制
      'i': this.getMinutes(), // 分
      's': this.getSeconds(), // 秒
      'S': this.getMilliseconds(), // 毫秒
      'w': this.getDay(), // 星期
      'W': Date.WEEK_NAMES[this.getDay()] // 星期 名称
    }
    forEach(marks, function (value, mark) {
      if (new RegExp('(' + mark + '{1,4})').test(format)) {
        const matched = RegExp.$1
        const len = matched.length
        let val = '' + value
        if (len > 1 && len > val.length && mark !== 'M' && mark !== 'W') {
          val = ('000' + val).substring(val.length + 3 - len)
        }
        if (fn) {
          val = fn(mark, value, val)
        }
        format = format.replace(matched, val)
      }
    })
    return format
  }
})
